Skip to content

Harden bridge transport and compatibility - #18

Open
Rerowros wants to merge 2 commits into
PasarGuard:mainfrom
Rerowros:codex/bridge-security-hardening
Open

Harden bridge transport and compatibility#18
Rerowros wants to merge 2 commits into
PasarGuard:mainfrom
Rerowros:codex/bridge-security-hardening

Conversation

@Rerowros

@Rerowros Rerowros commented Aug 9, 2026

Copy link
Copy Markdown

Summary

  • Reject HTTP redirects so node API credentials never follow an untrusted origin.
  • Bound and preserve pending sync work across reconnects; time-bound gRPC stream lifecycle and retry failures safely.
  • Redact and sanitize bridge logs, including exception text.
  • Restore REST/gRPC factory compatibility for api_port, max_message_size, and Controller.extra.

Validation

  • uv run python -m unittest discover -s tests -v (30 passed)
  • uv run python -m compileall -q PasarGuardNodeBridge tests
  • uv build
  • git diff --check

Risk / rollout notes

  • REST requests now reject all HTTP 3xx responses rather than following them.
  • Pending work is intentionally preserved on disconnect; use the explicit flush operation when clearing it is intended.

Summary by CodeRabbit

  • New Features

    • Added coordinated user-revocation workflows with leases, conflict handling, and fail-safe processing.
    • Added user-sync reconciliation, epoch protection, bounded storage, and queue-capacity errors.
    • Added optional API-port and gRPC message-size configuration.
    • Preserved synchronous metadata access for compatibility.
  • Bug Fixes

    • Improved synchronization recovery, reconnect behavior, pending-work preservation, and operation timeouts.
    • Disabled automatic redirects and strengthened HTTP error handling.
  • Security

    • Sanitized log output and strengthened redirect and stale-operation protection.
  • Documentation

    • Documented new synchronization, revocation, capacity, configuration, and compatibility options.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The change adds factory compatibility options, epoch-aware synchronization, bounded and revocation-aware storage, hardened HTTP and gRPC lifecycle handling, sanitized logging, and controller worker recovery. Tests cover leases, revocation, fencing, redirects, transport failures, cancellation, and reconnect behavior.

Changes

Node bridge synchronization

Layer / File(s) Summary
Public contracts and bounded storage
PasarGuardNodeBridge/__init__.py, PasarGuardNodeBridge/abstract_node.py, PasarGuardNodeBridge/storage.py, PasarGuardNodeBridge/common/*, tests/test_constructor_compatibility.py, tests/test_storage.py
create_node supports api_port and max_message_size. Public synchronization and revocation types are exported. Storage enforces capacity, generations, fencing, claim delays, and lease coordination. Protobuf messages carry synchronization epochs.
Controller revocation and worker recovery
PasarGuardNodeBridge/controller.py, tests/test_user_revocation.py, tests/test_security_hardening.py
The controller manages revocation lifecycle methods, user-sync leases, sanitized logs, lifecycle reconciliation, reconnect recovery, worker retirement, cancellation, partial failures, and claim requeue behavior.
HTTP and gRPC synchronization lifecycle
PasarGuardNodeBridge/aiohttp_compat.py, PasarGuardNodeBridge/grpclib.py, PasarGuardNodeBridge/rest.py, tests/test_security_hardening.py, tests/test_epoch_fencing.py, tests/test_stop_lifecycle.py
Redirects are disabled and treated as errors. REST and gRPC synchronization propagate epochs and revocation identifiers. Stream setup, sends, termination, responses, and cleanup use bounded handling.
Documentation and release metadata
README.md, pyproject.toml
The documentation describes factory options, synchronization semantics, revocation APIs, lease failures, capacity errors, and recovery behavior. The package version changes to 0.10.0.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RevocationClient
  participant Controller
  participant UserSyncStore
  participant NodeTransport
  RevocationClient->>Controller: begin_user_revocation
  Controller->>UserSyncStore: fence users and acquire lease
  Controller->>NodeTransport: synchronize users with revocation_id
  NodeTransport-->>Controller: return completion or failures
  Controller->>UserSyncStore: acknowledge, requeue, or retain claims
  RevocationClient->>Controller: finalize_user_revocation
  Controller->>UserSyncStore: finalize revocation
Loading

Possibly related PRs

Suggested reviewers: m03ed, immohammad20000

Poem

A rabbit checks each guarded claim,
Epochs keep the paths the same.
Streams close and leases stay,
Fenced updates wait their day.
Ports and logs are tidy too—
Revocation sees it through.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's transport hardening and REST/gRPC compatibility changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
tests/test_security_hardening.py (3)

337-340: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bound the disconnect() await so a regression fails instead of hanging.

await first.disconnect() has no time limit. This test depends on disconnect() cancelling the running _sync_worker task. If that cancellation regresses, the test blocks until the suite-level timeout rather than reporting a failure.

🧪 Proposed change
-        await first.disconnect()
+        await asyncio.wait_for(first.disconnect(), timeout=1.0)
         claimed = await second._claim_pending_users()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_security_hardening.py` around lines 337 - 340, Bound the await of
first.disconnect() in the test around _claim_pending_users so cancellation
regressions fail promptly instead of hanging; use the test suite’s existing
timeout utility or convention and preserve the subsequent claimed-user
assertions.

158-183: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the hand-built worker fixture.

This test assigns about eighteen attributes to a GrpcNode created with __new__. SharedStoreDisconnectTests._controller performs a similar setup. When _sync_worker starts reading a new attribute, these tests fail with AttributeError instead of a meaningful assertion, and each fixture must be updated separately.

Extract a shared module-level builder that both test classes call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_security_hardening.py` around lines 158 - 183, Extract the
repeated hand-built GrpcNode setup from
test_stream_open_timeout_increments_worker_failure_and_requeues and
SharedStoreDisconnectTests._controller into a shared module-level builder. Have
both tests call the builder, while preserving their scenario-specific overrides
and mocks, so newly required _sync_worker attributes are initialized in one
place.

261-281: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move this test out of LoggingSafetyTests.

test_connect_restarts_worker_to_discover_stored_pending_work verifies worker restart behavior on connect. It does not verify logging safety. Place it in a class that describes worker lifecycle so the suite stays navigable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_security_hardening.py` around lines 261 - 281, Move
test_connect_restarts_worker_to_discover_stored_pending_work out of
LoggingSafetyTests and into the existing test class covering worker lifecycle or
connect behavior. Keep the test setup, assertions, and mocking unchanged; only
relocate it to the semantically appropriate class.
tests/test_storage.py (1)

73-82: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider covering the claimed-user accounting and the constructor validation.

The new test covers pending-only accounting. Two behaviors added in this PR remain untested: enqueue_users counts claimed users toward the bound, and the constructor rejects a non-positive max_pending_users_per_node. Both are cheap to add.

🧪 Suggested additional tests
    async def test_claimed_users_count_toward_bound(self):
        store = InMemoryUserSyncStore(max_pending_users_per_node=1)
        await store.enqueue_users("node-1", [User(email="a@example.com")])
        await store.claim_users("node-1", "worker-1", limit=10, lease_seconds=30)

        with self.assertRaises(UserSyncStoreFullError):
            await store.enqueue_users("node-1", [User(email="b@example.com")])

    def test_non_positive_bound_is_rejected(self):
        with self.assertRaises(ValueError):
            InMemoryUserSyncStore(max_pending_users_per_node=0)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_storage.py` around lines 73 - 82, Add tests covering the remaining
constructor and accounting behavior in the storage test suite: add an async test
that claims the node’s only user and verifies enqueue_users rejects another user
because claimed users count toward max_pending_users_per_node, and add a
constructor test verifying InMemoryUserSyncStore rejects a zero or otherwise
non-positive bound with ValueError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@PasarGuardNodeBridge/aiohttp_compat.py`:
- Around line 31-33: Update Node.stream_logs in rest.py to invoke
raise_for_status for 3xx responses as well as 4xx/5xx responses, aligning its
pre-stream status check with BufferedStatusError’s 300-and-above policy.
Preserve normal streaming for 2xx responses so redirects produce NodeAPIError
instead of an empty log queue.

In `@PasarGuardNodeBridge/controller.py`:
- Around line 41-62: Update _sanitize_log_text to escape Unicode line separators
U+2028 and U+2029 in addition to the existing control characters. Modify
_SanitizingLoggerAdapter so the final rendered message is sanitized after
positional argument interpolation, preserving exception formatting and
truncation behavior; add tests covering positional arguments containing CR/LF
and U+2028.
- Around line 646-648: Extend the outer failure handling around the worker flow
to requeue any remaining claimed_users for all non-cancellation exceptions,
including failures from _ack_claimed_users(), _requeue_claimed_users(),
sync_users_chunked(), _sync_batch_users(), and _claim_pending_users(). Ensure
partial acknowledgment or requeue failures trigger explicit retry/requeue
handling so every still-claimed user is recovered before the worker exits.

In `@PasarGuardNodeBridge/grpclib.py`:
- Around line 435-446: Update the user-send loop in the SyncUser stream flow to
stop iterating after the first send_message failure. Keep the failed user in
failed, mark all remaining users as failed without retrying send_message, and
preserve the existing warning for the initial stream error.
- Around line 142-153: Update _open_grpc_stream so cancellation or timeout
during method.open’s context entry still closes the partially established gRPC
stream; do not rely solely on AsyncExitStack.enter_async_context registering
__aexit__ after __aenter__ completes. Explicitly retain and clean up the
stream/context manager using the appropriate grpclib lifecycle methods, while
preserving the bounded establishment and cleanup timeouts.

In `@PasarGuardNodeBridge/storage.py`:
- Around line 141-144: The new store-capacity failure must have a consistent
public error contract. Update Controller.update_user and update_users to catch
the relevant initialization/enqueue exceptions and convert them to NodeAPIError,
or explicitly document that these methods propagate the RuntimeError subclasses;
preserve the chosen behavior consistently for both methods.

---

Nitpick comments:
In `@tests/test_security_hardening.py`:
- Around line 337-340: Bound the await of first.disconnect() in the test around
_claim_pending_users so cancellation regressions fail promptly instead of
hanging; use the test suite’s existing timeout utility or convention and
preserve the subsequent claimed-user assertions.
- Around line 158-183: Extract the repeated hand-built GrpcNode setup from
test_stream_open_timeout_increments_worker_failure_and_requeues and
SharedStoreDisconnectTests._controller into a shared module-level builder. Have
both tests call the builder, while preserving their scenario-specific overrides
and mocks, so newly required _sync_worker attributes are initialized in one
place.
- Around line 261-281: Move
test_connect_restarts_worker_to_discover_stored_pending_work out of
LoggingSafetyTests and into the existing test class covering worker lifecycle or
connect behavior. Keep the test setup, assertions, and mocking unchanged; only
relocate it to the semantically appropriate class.

In `@tests/test_storage.py`:
- Around line 73-82: Add tests covering the remaining constructor and accounting
behavior in the storage test suite: add an async test that claims the node’s
only user and verifies enqueue_users rejects another user because claimed users
count toward max_pending_users_per_node, and add a constructor test verifying
InMemoryUserSyncStore rejects a zero or otherwise non-positive bound with
ValueError.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 42487186-7352-4846-af7d-600c610fb7fb

📥 Commits

Reviewing files that changed from the base of the PR and between 2b37c95 and 2b2c385.

📒 Files selected for processing (10)
  • PasarGuardNodeBridge/__init__.py
  • PasarGuardNodeBridge/aiohttp_compat.py
  • PasarGuardNodeBridge/controller.py
  • PasarGuardNodeBridge/grpclib.py
  • PasarGuardNodeBridge/rest.py
  • PasarGuardNodeBridge/storage.py
  • README.md
  • tests/test_constructor_compatibility.py
  • tests/test_security_hardening.py
  • tests/test_storage.py

Comment thread PasarGuardNodeBridge/aiohttp_compat.py
Comment thread PasarGuardNodeBridge/controller.py
Comment thread PasarGuardNodeBridge/controller.py Outdated
Comment thread PasarGuardNodeBridge/grpclib.py
Comment thread PasarGuardNodeBridge/grpclib.py
Comment thread PasarGuardNodeBridge/storage.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_security_hardening.py`:
- Around line 443-467: Update test_outer_worker_failure_retries_failed_requeue
so the worker terminates after the recovery retry: either set the controller
shutdown event during the mocked backoff or run _sync_worker as a cancellable
task and cancel it before awaiting completion. Preserve the existing assertions
that the requeue is attempted twice and the user is recoverable.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bcc47fa7-c057-49c3-8d57-ae61339f43ec

📥 Commits

Reviewing files that changed from the base of the PR and between 2b2c385 and e5316b2.

📒 Files selected for processing (6)
  • PasarGuardNodeBridge/controller.py
  • PasarGuardNodeBridge/grpclib.py
  • PasarGuardNodeBridge/rest.py
  • README.md
  • tests/test_security_hardening.py
  • tests/test_storage.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • README.md
  • PasarGuardNodeBridge/rest.py
  • PasarGuardNodeBridge/grpclib.py
  • PasarGuardNodeBridge/controller.py

Comment thread tests/test_security_hardening.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
PasarGuardNodeBridge/controller.py (1)

748-753: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the !s conversion flag for the exception text.

Ruff reports RUF010 on Line 751. The surrounding code already uses {e!s} in other log statements, for example in _cleanup_sync_worker at Line 532.

♻️ Proposed fix
-                f"[{self.name}] Unexpected error in sync worker | Error: {error_type} - {str(e)}", exc_info=True
+                f"[{self.name}] Unexpected error in sync worker | Error: {error_type} - {e!s}", exc_info=True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PasarGuardNodeBridge/controller.py` around lines 748 - 753, Update the
unexpected-error log in the sync worker’s exception handler to use the `!s`
conversion flag when formatting the exception text, while preserving the
existing error type, message context, and `exc_info=True` behavior.

Source: Linters/SAST tools

tests/test_security_hardening.py (1)

526-574: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce timing sensitivity in the lease-expiry test.

The test depends on wall-clock margins that are small. first._sync_lease_seconds is 0.08, and Line 561 sleeps 0.02 before asserting that the second worker has not processed the claim. On a loaded CI runner, the second worker can claim the expired lease before that assertion runs, which makes the test flaky.

Increase the lease duration and the observation window so the margin between "lease still held" and "lease expired" is larger.

♻️ Proposed timing adjustment
-        first._sync_lease_seconds = 0.08
+        first._sync_lease_seconds = 0.5
...
-        await asyncio.sleep(0.02)
+        await asyncio.sleep(0.1)
         self.assertFalse(second_processed.is_set())
         self.assertFalse(second_worker.done())
 
-        await asyncio.wait_for(second_processed.wait(), timeout=0.5)
+        await asyncio.wait_for(second_processed.wait(), timeout=2.0)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_security_hardening.py` around lines 526 - 574, Adjust the timing
constants in test_second_worker_wakes_after_failed_requeue_lease_expires to
increase the lease duration and lengthen the pre-expiry observation delay,
preserving the assertion that the second worker has not processed the claim
before expiration and the existing post-expiry wait behavior.
tests/test_storage.py (1)

119-122: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the negative boundary too.

The test name says “non-positive,” but it only checks 0. Add -1 so regressions in the validation condition are detected.

Suggested test adjustment
     def test_non_positive_per_node_bound_is_rejected(self):
-        with self.assertRaises(ValueError):
-            InMemoryUserSyncStore(max_pending_users_per_node=0)
+        for limit in (0, -1):
+            with self.subTest(limit=limit):
+                with self.assertRaises(ValueError):
+                    InMemoryUserSyncStore(max_pending_users_per_node=limit)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_storage.py` around lines 119 - 122, Update
test_non_positive_per_node_bound_is_rejected to also construct
InMemoryUserSyncStore with max_pending_users_per_node=-1 inside the ValueError
assertion, covering both zero and negative non-positive bounds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@PasarGuardNodeBridge/controller.py`:
- Around line 565-573: Update _wait_for_claim_recheck so wait_delay is bounded
below by _sync_poll_interval, while still preventing negative delays. Keep the
existing event wait, timeout handling, and wake-up behavior unchanged.

In `@tests/test_storage.py`:
- Around line 87-99: Update
test_next_claim_delay_distinguishes_empty_pending_and_leased_work to avoid
timing flakiness by using a substantially longer lease_seconds value, or by
controlling time.monotonic() deterministically; preserve the assertions that
empty work returns None, pending work returns 0.0, and leased work returns a
positive delay no greater than the lease.

---

Nitpick comments:
In `@PasarGuardNodeBridge/controller.py`:
- Around line 748-753: Update the unexpected-error log in the sync worker’s
exception handler to use the `!s` conversion flag when formatting the exception
text, while preserving the existing error type, message context, and
`exc_info=True` behavior.

In `@tests/test_security_hardening.py`:
- Around line 526-574: Adjust the timing constants in
test_second_worker_wakes_after_failed_requeue_lease_expires to increase the
lease duration and lengthen the pre-expiry observation delay, preserving the
assertion that the second worker has not processed the claim before expiration
and the existing post-expiry wait behavior.

In `@tests/test_storage.py`:
- Around line 119-122: Update test_non_positive_per_node_bound_is_rejected to
also construct InMemoryUserSyncStore with max_pending_users_per_node=-1 inside
the ValueError assertion, covering both zero and negative non-positive bounds.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 30391eeb-0cbb-4b2e-9ee1-58f4949609e2

📥 Commits

Reviewing files that changed from the base of the PR and between e5316b2 and 3702b86.

📒 Files selected for processing (6)
  • PasarGuardNodeBridge/controller.py
  • PasarGuardNodeBridge/rest.py
  • PasarGuardNodeBridge/storage.py
  • README.md
  • tests/test_security_hardening.py
  • tests/test_storage.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • PasarGuardNodeBridge/storage.py

Comment thread PasarGuardNodeBridge/controller.py
Comment thread tests/test_storage.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
PasarGuardNodeBridge/controller.py (3)

761-772: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

A very small positive delay still bypasses the backoff floor.

wait_delay applies the floor only when delay is not greater than zero. A store that reports a small positive deadline, for example 0.0005, produces a tight claim loop with no effective pause. Apply the floor to every delay.

🐛 Proposed fix
-        wait_delay = delay if delay > 0 else max(self._sync_poll_interval, MIN_CLAIM_RECHECK_DELAY)
+        wait_delay = max(delay, MIN_CLAIM_RECHECK_DELAY) if delay > 0 else max(
+            self._sync_poll_interval, MIN_CLAIM_RECHECK_DELAY
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PasarGuardNodeBridge/controller.py` around lines 761 - 772, Update
_wait_for_claim_recheck so wait_delay always applies the minimum backoff floor,
including when delay is a small positive value; retain the configured
_sync_poll_interval as the other floor input and preserve the existing
event-wait behavior.

684-701: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the sync-worker cleanup above the recovery timeout.

_cleanup_sync_worker uses a 2.0 second maximum, while _recover_claimed_users uses CLAIM_RECOVERY_TIMEOUT = 1.0. If the worker task can still execute recovery during cleanup, raise this cleanup bound enough above the fixed recovery timeout, or derive it from CLAIM_RECOVERY_TIMEOUT, so cleanup does not time out while the worker task is still scheduled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PasarGuardNodeBridge/controller.py` around lines 684 - 701, The timeout in
_cleanup_sync_worker must exceed the fixed CLAIM_RECOVERY_TIMEOUT used by
_recover_claimed_users. Update the cleanup wait bound to derive from
CLAIM_RECOVERY_TIMEOUT or otherwise provide sufficient margin, ensuring the
worker can finish recovery before cleanup times out.

968-1016: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Don’t retain the whole execution lease for known failed users.

_sync_batch_users returns only the failed users, and those keys are put back in failed_claims. Then _abandon_user_sync_lease stores the lease for all user_keys, so any concurrent begin_user_revocation(["X","Y"], ...) must wait for or fail against the same fail-closed lease, even though only Y has an unknown outcome. Release or split the lease for the acknowledged/failed users and keep it only for keys whose remote outcome is genuinely unknown.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PasarGuardNodeBridge/controller.py` around lines 968 - 1016, Update the
partial-failure path around _sync_batch_users so _abandon_user_sync_lease does
not retain the lease for every user key. Release or split the lease after
deriving failed_claims, removing acknowledged and known-failed users; retain it
only for keys whose remote outcome is genuinely unknown. Keep the existing
acknowledgment and requeue behavior intact, and ensure the exception path still
abandons the lease for genuinely unresolved outcomes.
🧹 Nitpick comments (2)
tests/test_user_revocation.py (1)

20-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share the hand-built controller factory between test modules.

This _controller helper builds a Controller with object.__new__ and sets 21 private attributes by hand. tests/test_security_hardening.py defines a near-identical helper in SharedStoreDisconnectTests._controller. The two copies already differ: this one omits _tasks, _task_lock, and _version_lock.

When Controller.__init__ or _sync_worker starts using a new attribute, both copies must be updated, and a missed update surfaces as an AttributeError inside the worker rather than a clear failure. Move the factory into a shared test helper module.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_user_revocation.py` around lines 20 - 43, Move the hand-built
Controller factory from this test module into a shared test helper, then update
both this module and SharedStoreDisconnectTests._controller to import and reuse
it. Preserve the existing setup while consolidating all required private
attributes, including _tasks, _task_lock, and _version_lock, so future
Controller changes require updates in only one factory.
tests/test_security_hardening.py (1)

437-443: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

_wait_until spins the event loop instead of yielding time.

await asyncio.sleep(0) yields control but schedules an immediate callback. The loop therefore runs at full CPU for up to timeout. test_idle_retirement_boundary_100x_never_strands_enqueued_work calls this helper 100 times, so the cost accumulates.

Use a small positive sleep so the loop can idle between checks.

♻️ Proposed change
     `@staticmethod`
     async def _wait_until(predicate, timeout=0.2):
         async def poll():
             while not predicate():
-                await asyncio.sleep(0)
+                await asyncio.sleep(0.001)
 
         await asyncio.wait_for(poll(), timeout=timeout)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_security_hardening.py` around lines 437 - 443, Update the
_wait_until helper’s poll loop to await a small positive sleep interval instead
of asyncio.sleep(0), allowing the event loop to idle between predicate checks
while preserving the existing timeout and polling behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@PasarGuardNodeBridge/storage.py`:
- Around line 435-479: Update abort_user_revocation and finalize_user_revocation
to restore each affected state's closing flag and ownership/finalization fields
when _wait_for_user_sync_leases raises UserSyncLeaseLostError or cancellation,
then re-raise the exception. Add regression tests covering failed lease drains
for both methods and verify the owning revocation_id can acquire a user-sync
lease afterward.

In `@tests/test_security_hardening.py`:
- Around line 608-660: Increase the scheduling margins in
test_idle_retirement_boundary_100x_never_strands_enqueued_work and
test_zero_deadline_worker_cancels_without_hot_loop_or_task_leak so loaded CI
does not fail nondeterministically. Raise the short sleeps, per-operation
timeouts, and claim-count allowance as needed, or reduce the retirement test
iteration count while preserving its boundary and task-cleanup assertions.

---

Outside diff comments:
In `@PasarGuardNodeBridge/controller.py`:
- Around line 761-772: Update _wait_for_claim_recheck so wait_delay always
applies the minimum backoff floor, including when delay is a small positive
value; retain the configured _sync_poll_interval as the other floor input and
preserve the existing event-wait behavior.
- Around line 684-701: The timeout in _cleanup_sync_worker must exceed the fixed
CLAIM_RECOVERY_TIMEOUT used by _recover_claimed_users. Update the cleanup wait
bound to derive from CLAIM_RECOVERY_TIMEOUT or otherwise provide sufficient
margin, ensuring the worker can finish recovery before cleanup times out.
- Around line 968-1016: Update the partial-failure path around _sync_batch_users
so _abandon_user_sync_lease does not retain the lease for every user key.
Release or split the lease after deriving failed_claims, removing acknowledged
and known-failed users; retain it only for keys whose remote outcome is
genuinely unknown. Keep the existing acknowledgment and requeue behavior intact,
and ensure the exception path still abandons the lease for genuinely unresolved
outcomes.

---

Nitpick comments:
In `@tests/test_security_hardening.py`:
- Around line 437-443: Update the _wait_until helper’s poll loop to await a
small positive sleep interval instead of asyncio.sleep(0), allowing the event
loop to idle between predicate checks while preserving the existing timeout and
polling behavior.

In `@tests/test_user_revocation.py`:
- Around line 20-43: Move the hand-built Controller factory from this test
module into a shared test helper, then update both this module and
SharedStoreDisconnectTests._controller to import and reuse it. Preserve the
existing setup while consolidating all required private attributes, including
_tasks, _task_lock, and _version_lock, so future Controller changes require
updates in only one factory.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ee711d6-dd3f-4563-bdb2-85e6e2ba1cc8

📥 Commits

Reviewing files that changed from the base of the PR and between 3702b86 and 3b0f230.

📒 Files selected for processing (10)
  • PasarGuardNodeBridge/__init__.py
  • PasarGuardNodeBridge/abstract_node.py
  • PasarGuardNodeBridge/controller.py
  • PasarGuardNodeBridge/grpclib.py
  • PasarGuardNodeBridge/rest.py
  • PasarGuardNodeBridge/storage.py
  • README.md
  • tests/test_security_hardening.py
  • tests/test_storage.py
  • tests/test_user_revocation.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_storage.py

Comment thread PasarGuardNodeBridge/storage.py
Comment thread tests/test_security_hardening.py
@Rerowros
Rerowros force-pushed the codex/bridge-security-hardening branch from 15004a8 to d1001ba Compare August 10, 2026 04:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
tests/test_security_hardening.py (1)

686-691: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add an explicit strict= argument to zip().

Ruff flags B905 here. The two iterables differ in length by design, so strict=True would raise. Pass strict=False to make the intent explicit and clear the lint warning.

♻️ Proposed change
-            self.assertTrue(all(b > a for a, b in zip(store.claim_times, store.claim_times[1:])))
+            self.assertTrue(
+                all(b > a for a, b in zip(store.claim_times, store.claim_times[1:], strict=False))
+            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_security_hardening.py` around lines 686 - 691, Update the zip()
call in the claim_times ordering assertion to pass strict=False explicitly,
preserving the intentional behavior for iterables of differing lengths and
clearing Ruff B905.

Source: Linters/SAST tools

PasarGuardNodeBridge/abstract_node.py (1)

70-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Adding an abstract reconcile_users breaks external subclasses.

PasarGuardNode is exported from PasarGuardNodeBridge/__init__.py. Any third-party subclass that does not define reconcile_users now fails to instantiate with TypeError. The other changes in this file are additive keyword parameters and stay compatible.

If backward compatibility matters for this release, provide a default implementation that raises NodeAPIError(501, ...) instead of marking it abstract, and document the new method in the release notes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PasarGuardNodeBridge/abstract_node.py` around lines 70 - 77, The abstract
reconcile_users method on PasarGuardNode breaks instantiation of existing
external subclasses. Remove the `@abstractmethod` requirement and provide a
default implementation that raises NodeAPIError with HTTP status 501, preserving
the shown signature; document the new method in the release notes.
tests/test_epoch_fencing.py (1)

45-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Bind the loop variables in the closures.

response_for and grpc_request read captured and info_method from the enclosing loop scope. Both are awaited inside the same iteration, so the current test passes. Ruff still reports B023 for both closures, and the sibling tests in tests/test_user_revocation.py already use the default-argument idiom (for example async def transport(_captured=captured, **kwargs)).

Bind the values as default arguments for consistency and to clear the lint finding.

♻️ Proposed change
-                async def response_for(request):
+                async def response_for(request, _captured=captured):
                     if request is None:
                         return service.BaseInfoResponse(
                             started=False,
                             user_sync_epoch_supported=True,
                             user_sync_epoch=40,
                         )
-                    captured.append(request.user_sync_epoch)
+                    _captured.append(request.user_sync_epoch)
@@
-                    async def grpc_request(**kwargs):
-                        request = None if kwargs["method"] is info_method else kwargs["request"]
+                    async def grpc_request(_info_method=info_method, **kwargs):
+                        request = None if kwargs["method"] is _info_method else kwargs["request"]
                         return await response_for(request)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_epoch_fencing.py` around lines 45 - 76, Update the response_for
and grpc_request closures in the node-type loop to bind captured loop values
through default arguments, including captured for response_for and info_method
for grpc_request. Preserve their existing request handling and responses while
eliminating the B023 late-binding lint findings.

Source: Linters/SAST tools

tests/test_user_revocation.py (1)

23-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Set _user_sync_epoch_capability_probed explicitly in _controller.

_controller sets _user_sync_epoch_supported = True but leaves _user_sync_epoch_capability_probed unset. The tests pass only because _probe_user_sync_epoch_capability reads the flag with getattr(..., False) and then finds no info attribute on a bare Controller, so it marks the probe complete and returns.

The tests therefore depend on defensive fallbacks rather than on declared state. tests/test_epoch_fencing.py already sets both flags in _configured_node.

♻️ Proposed change
     controller._user_sync_epoch_supported = True
+    controller._user_sync_epoch_capability_probed = True
+    controller._user_sync_epoch_handshake_lock = asyncio.Lock()
+    controller._user_sync_connection_generation = 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_user_revocation.py` around lines 23 - 47, Update the test helper
_controller to initialize _user_sync_epoch_capability_probed explicitly
alongside _user_sync_epoch_supported, using the same intended initial state as
_configured_node in tests/test_epoch_fencing.py. Keep the helper’s other
controller state unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@PasarGuardNodeBridge/controller.py`:
- Around line 333-347: Update the cancellation cleanup in
_release_user_sync_lease, _abandon_user_sync_lease, and _release_lifecycle_lease
so awaiting the cancelled heartbeat/task only suppresses CancelledError caused
by that task’s own cancellation; detect and re-raise caller cancellation
instead, preserving normal cleanup and heartbeat error handling.

In `@PasarGuardNodeBridge/storage.py`:
- Around line 638-682: Replace _revocation_state calls in
acquire_startup_user_sync_lease and the corresponding path around line 729 with
read-only lookups that do not create entries for unseen keys; preserve
default-state behavior when no stored state exists. Ensure cleanup removes fully
default _UserRevocationState entries when the node has no active revocation, so
self._revocations remains bounded under user churn.
- Around line 748-776: Update the lease-narrowing flow in
_retain_unknown_user_sync_lease_keys so the heartbeat task uses the returned
narrowed UserSyncLease as well as user_sync_lease. Ensure both lease references
are updated atomically after retain_user_sync_lease_keys succeeds, preventing
_heartbeat_user_sync_lease from retaining the stale preemptive lease.

In `@tests/test_security_hardening.py`:
- Around line 851-878: Update
test_partial_ack_then_failed_requeue_recovers_only_failed_claim to patch
asyncio.sleep with a side effect that sets the controller’s _shutdown_event
after the recovery attempt, matching the sibling tests’ shutdown pattern. Keep
the existing requeue_calls and recovered assertions unchanged.

---

Nitpick comments:
In `@PasarGuardNodeBridge/abstract_node.py`:
- Around line 70-77: The abstract reconcile_users method on PasarGuardNode
breaks instantiation of existing external subclasses. Remove the `@abstractmethod`
requirement and provide a default implementation that raises NodeAPIError with
HTTP status 501, preserving the shown signature; document the new method in the
release notes.

In `@tests/test_epoch_fencing.py`:
- Around line 45-76: Update the response_for and grpc_request closures in the
node-type loop to bind captured loop values through default arguments, including
captured for response_for and info_method for grpc_request. Preserve their
existing request handling and responses while eliminating the B023 late-binding
lint findings.

In `@tests/test_security_hardening.py`:
- Around line 686-691: Update the zip() call in the claim_times ordering
assertion to pass strict=False explicitly, preserving the intentional behavior
for iterables of differing lengths and clearing Ruff B905.

In `@tests/test_user_revocation.py`:
- Around line 23-47: Update the test helper _controller to initialize
_user_sync_epoch_capability_probed explicitly alongside
_user_sync_epoch_supported, using the same intended initial state as
_configured_node in tests/test_epoch_fencing.py. Keep the helper’s other
controller state unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a502520-14e7-4e67-8b4c-569038c5fed3

📥 Commits

Reviewing files that changed from the base of the PR and between 3b0f230 and d1001ba.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • PasarGuardNodeBridge/__init__.py
  • PasarGuardNodeBridge/abstract_node.py
  • PasarGuardNodeBridge/common/service.proto
  • PasarGuardNodeBridge/common/service_pb2.py
  • PasarGuardNodeBridge/common/service_pb2.pyi
  • PasarGuardNodeBridge/controller.py
  • PasarGuardNodeBridge/grpclib.py
  • PasarGuardNodeBridge/rest.py
  • PasarGuardNodeBridge/storage.py
  • README.md
  • pyproject.toml
  • tests/test_epoch_fencing.py
  • tests/test_security_hardening.py
  • tests/test_stop_lifecycle.py
  • tests/test_storage.py
  • tests/test_user_revocation.py

Comment thread PasarGuardNodeBridge/controller.py
Comment thread PasarGuardNodeBridge/storage.py
Comment thread PasarGuardNodeBridge/storage.py
Comment thread tests/test_security_hardening.py
@Rerowros
Rerowros force-pushed the codex/bridge-security-hardening branch from d1001ba to 38c4ac0 Compare August 10, 2026 05:11
@Rerowros
Rerowros force-pushed the codex/bridge-security-hardening branch from 38c4ac0 to bb50322 Compare August 10, 2026 05:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant